<?php

namespace app\service;


/**
 * Redis 分布式锁
 * redis-version: >2.6.12
 */
class RedisLock {

    /*
    * 锁实现的注意点:
    * 1互斥: 任意时刻, 只能有一个客户端获得锁
    * 2不会死锁: 客户端持有锁期间崩溃, 没有主动解除锁, 能保证后续的其他客户端获得锁
    * 3锁归属标识: 加锁和解锁的必须是同一个客户端, 客户端不能解掉非自己持有的锁(锁应具备标识)
    */
    // 参数解释
    // $value 加锁的客户端请求标识, 必须保证在所有获取锁请求的客户端里保持唯一, 满足上面的第3个条件: 加锁/解锁的是同一客户端
    // "NX" 仅在key不存在时加锁, 满足条件1: 互斥型
    // "EX" 设置锁过期时间, 满足条件2: 避免死锁
    // $redis->set($key, $value, ["NX", "EX" => $expire])

    //private $config; //Redis配置
    static $lockKey; //锁的键名
    static $timeout; //超时时间
    static $redis; //Redis实例
    static $isNegative = true; //是否是悲观锁
    static $identify = ""; //锁的唯一标识，防止锁被别的进程误删

    public function __construct($lockKey, $timeout = 5, $isNegative = false) {
        if (!$lockKey) {
            die("lockKey不能为空");
        }

        try {
            self::$lockKey = $lockKey;
            self::$isNegative = $isNegative;
            self::$timeout = $timeout;
            self::$redis = redis();
        } catch (\Exception $e) {
            die('error-RedisConnect' . $e->getCode() . $e->getMessage());
        }
    }

    /**
     * 加锁
     */
    public function lock() {
        $val = md5(uniqid(mt_rand(), true));
        if (self::$isNegative) { // 悲观锁
            $endTime = microtime(true) * 1000 + self::$timeout * 1000;
            while (microtime(true) * 1000 < $endTime) {
                $acquired = self::$redis->set(self::$lockKey, $val, ['nx', 'ex' => self::$timeout]);
                if ($acquired) {
                    self::$identify = $val;
                    return true;
                }
                usleep(100);
            }
        } else { // 乐观锁: 只尝试一次，成功返回true,失败返回false
            $acquired = self::$redis->set(self::$lockKey, $val, ['nx', 'ex' => self::$timeout]);
            if ($acquired) {
                self::$identify = $val;
                return true;
            }
        }
        return false;
    }

    /**
     * 释放锁
     */
    public function unlock() {
        // 没有成功获得锁，直接返回false
        if (!self::$identify) {
            return false;
        }

        // 由于判断是否相等和删除是两步操作，因此使用lua脚本来保证原子性
        // redis.call第一个参数是要调用的redis命令，KEYS和ARGV是lua脚本中的table类型全局变量，索引是从1开始
        // 如果根据key拿到的value跟传入的value相同就执行del，否则就返回0
        $luaScript = "if redis.call('get', KEYS[1]) == ARGV[1] then return redis.call('del', KEYS[1]) else return 0 end";
        // eval方法的参数 3个，第一个是脚本代码，第二个是一个参数数组，第三个参数是个整数，表示第二个参数数组中的前几个是key参数，剩下的都是附加参数
        $result = self::$redis->eval($luaScript, [self::$lockKey, self::$identify], 1);
        if ($result) {
            return true;
        }
        return false;
    }
}
